﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BobcatUtility;
using CNUtility;
using System.ServiceModel;
using CNShapes;
using System.ServiceModel.Channels;

namespace CNNetworking
{
    /// <summary>
    /// This class handles the client end of the network communication. It can
    /// send messages via the IClientToServer interfaces, and receive callbacks
    /// via the IServerToClient callback-interface.
    /// 
    /// Communication between the client and the server uses WCF.
    /// 
    /// This class is a singleton.
    /// </summary>
    public class NetworkingClient : IServerToClient
    {
        #region Events

        /// <summary>
        /// Data passed with the heartbeat event.
        /// </summary>
        public class HeartbeatArgs : EventArgs
        {
            public IList<string> ClientNames { get; set; }
        }
        /// <summary>
        /// Event raised when we receive a heartbeat from the server.
        /// </summary>
        public event EventHandler<HeartbeatArgs> HeartbeatEvent;

        /// <summary>
        /// Data passed with the client-list updated event.
        /// </summary>
        public class ClientListUpdatedArgs : EventArgs
        {
            public IList<string> ClientNames { get; set; }
        }
        /// <summary>
        /// Event raised when the list of clients connected to the current session changes.
        /// </summary>
        public event EventHandler<ClientListUpdatedArgs> ClientListUpdatedEvent;

        #endregion

        #region Public properties

        /// <summary>
        /// Property to get the unique (GUID) ID of this client.
        /// </summary>
        public Guid ClientID
        {
            get { return m_ClientID; }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Singleton's getInstance method.
        /// </summary>
        public static NetworkingClient getInstance()
        {
            if (m_Instance == null)
            {
                m_Instance = new NetworkingClient();
            }
            return m_Instance;
        }

        /// <summary>
        /// Returns the connection-settings for this client.
        /// </summary>
        public ConnectionSettings getClientConnectionSettings()
        {
            ConnectionSettings connectionSettings = new ConnectionSettings();
            connectionSettings.FirstName = UserPreferences.getPropertyBag().getString("UserInfo.FirstName");
            connectionSettings.LastName = UserPreferences.getPropertyBag().getString("UserInfo.LastName");
            connectionSettings.HostName = Environment.MachineName;
            return connectionSettings;
        }

        /// <summary>
        /// We connect to the server whose settings are passed in.
        /// </summary>
        public void connect(ConnectionSettings serverSettings)
        {
            // We create the endpoint address to connect to...
            int iPort = UserPreferences.getPropertyBag().getInt("Networking.Port");
            string strEndpoint = "net.tcp://" + serverSettings.HostName + ":" + iPort.ToString() + "/Whiteboard/";
            Uri uri = new Uri(strEndpoint);

            // We need to create an "identity" for security reasons. (I don't really understand this,
            // but if you  don't do it you can't connect between machines on a LAN.)
            SpnEndpointIdentity identity = new SpnEndpointIdentity("");

            // We create the endpoint and connect...
            EndpointAddress serverAddress = new EndpointAddress(uri, identity, new AddressHeaderCollection());
            m_Server = new DuplexChannelFactory<IClientToServer>(this, "DefaultEndpoint", serverAddress).CreateChannel();

            // We notify the server that we want to connect, and provide our details...
            ConnectionSettings connectionSettings = getClientConnectionSettings();
            string strName = connectionSettings.FirstName + " " + connectionSettings.LastName;
            m_Server.connect(m_ClientID, strName);
        }

        /// <summary>
        /// We send a message to the server to tell it that we are disconnecting.
        /// </summary>
        public void disconnect()
        {
            if (m_Server == null)
            {
                return;
            }
            m_Server.disconnect(m_ClientID);
            m_Server = null;
        }

        #endregion

        #region IServerToClient Members

        /// <summary>
        /// Called by the server to tell us that it is disconnecting.
        /// </summary>
        public void onDisconnected()
        {
            m_Server = null;
        }

        /// <summary>
        /// Called when the server sends us a heartbeat. This is a two-way message (though
        /// it doesn't send any data back). Just returning successfully is enough to let
        /// the server know we're still alive.
        /// </summary>
        public void onHeartbeat()
        {
            try
            {
                Utility.CrossThreadInvoke(HeartbeatEvent, this, new HeartbeatArgs());
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called when the client list (i.e. the list of people connected to the same
        /// session) is updated.
        /// </summary>
        public void onClientListUpdated(IList<string> clientNames)
        {
            try
            {
                Utility.CrossThreadInvoke(ClientListUpdatedEvent, this, new ClientListUpdatedArgs { ClientNames = clientNames });
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server to request that we share our server-list with it.
        /// </summary>
        public void requestServerConnectionList()
        {
            try
            {
                IList<ConnectionSettings> results;

                // We check if wee are set up to share our settings...
                bool bShareSettings = UserPreferences.getPropertyBag().getBool("Networking.ShareServerList");
                if (bShareSettings == true)
                {
                    // We are sharing the list, so we get it from the preferences...
                    results = UserPreferences.getPropertyBag().getList<ConnectionSettings>("Networking.ServerConnectionList");
                }
                else
                {
                    // We are not sharing the list, so we return an empty list...
                    results = new List<ConnectionSettings>();
                }
                m_Server.sendServerConnectionList(results);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called when we receive a new set of connection settings. These will be the
        /// settings from one of the other clients in the system. We merge them with our
        /// current set.
        /// </summary>
        public void onNewServerConnectionList(IList<ConnectionSettings> connectionSettings)
        {
            try
            {
                // We see if this list contains any settings that we do not already hold. If it
                // does, we add them to our list.

                // To do this more efficiently, we put the existing data into a dictionary (we'd
                // use a set if C# had one!)...
                IList<ConnectionSettings> listCurrentConnectionSettings = UserPreferences.getPropertyBag().getList<ConnectionSettings>("Networking.ServerConnectionList");
                IDictionary<string, bool> setCurrentConnectionSettings = new Dictionary<string, bool>();
                foreach (ConnectionSettings cs in listCurrentConnectionSettings)
                {
                    setCurrentConnectionSettings.Add(cs.ToString(), true);
                }

                // Now we look through the collection of connection-settings passed in
                // to see if any of them are new...
                foreach (ConnectionSettings cs in connectionSettings)
                {
                    if (setCurrentConnectionSettings.ContainsKey(cs.ToString()) == false)
                    {
                        // We don't already have this connection-setting, so
                        // we'll add it to our collection...
                        listCurrentConnectionSettings.Add(cs);
                    }
                }

                // We store the new list back in the properties...
                UserPreferences.getPropertyBag().setList("Networking.ServerConnectionList", listCurrentConnectionSettings);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server to tell us that a new session has started.
        /// </summary>
        public void onNewSession(Guid sessionID)
        {
            try
            {
                // We clear the current session and set its ID to the
                // one passed in...
                Session session = Session.getInstance();
                session.newSession(sessionID);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server when a page is added to the session.
        /// </summary>
        public void onNewPage(Guid sessionID, Guid pageID)
        {
            try
            {
                checkSession(sessionID);

                // We add a page with the ID passed in to our session...
                Page page = Session.getInstance().addPage(pageID);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server to give us the ID of the new active page.
        /// </summary>
        public void onActivePageChanged(Guid sessionID, Guid pageID)
        {
            try
            {
                checkSession(sessionID);
                Session.getInstance().ActivePageID = pageID;
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server when a shape is added to the specified page.
        /// </summary>
        public void onNewShape(Guid sessionID, Guid pageID, CNShapes.Shape shape)
        {
            try
            {
                checkSession(sessionID);

                // We find the page with the ID passed in and add the shape to it...
                Page page = Session.getInstance().getPage(pageID);
                page.addShape(shape);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Called by the server when a shape has been updated on the specified page.
        /// </summary>
        public void onUpdatedShape(Guid sessionID, Guid pageID, Shape shape)
        {
            try
            {
                checkSession(sessionID);

                // We find the page with the ID passed in and add the shape to it...
                Page page = Session.getInstance().getPage(pageID);
                page.removeShape(shape);
                page.addShape(shape);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Private constructor (for singleton).
        /// </summary>
        private NetworkingClient()
        {
            m_ClientID = Guid.NewGuid();

            // We observe the session so that we can sent updates to the server...
            Session session = Session.getInstance();
            session.ShapeEditCompletedEvent += onShapeEditCompleted;
        }

        /// <summary>
        /// Called when a shape has been edited.
        /// </summary>
        private void onShapeEditCompleted(object sender, Session.Args e)
        {
            // We send the update to the server...
            if (m_Server == null)
            {
                return;
            }
            try
            {
                Session session = (Session)sender;
                m_Server.shapeEdited(m_ClientID, session.ID, e.Page.ID, e.Shape);
            }
            catch (Exception ex)
            {
                Logger.getInstance().log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// Checks that the active session is the one with the ID passed in.
        /// Throws an exception if it isn't.
        /// </summary>
        private void checkSession(Guid sessionID)
        {
            if (Session.getInstance().ID != sessionID)
            {
                throw new Exception("Active session does not match ID.");
            }
        }

        #endregion

        #region Private data

        private Guid m_ClientID;
        private static NetworkingClient m_Instance = null;
        private IClientToServer m_Server = null;

        #endregion
    }
}
